package sim.app.geo.haiti;



import java.util.ArrayList;
import sim.app.geo.haiti.HaitiFood.Node;
import sim.engine.SimState;
import sim.engine.Steppable;
import sim.engine.Stoppable;
import sim.field.grid.IntGrid2D;
import sim.util.Bag;



/**
 * AGENT
 * <p/>
 * This class holds the attributes and behavior of the individuals who live in
 * the environment. They are motivated by their energy levels, seeking to
 * maximize their energy over the course of the simulation.
 * <p/>
 * Agents can choose to move toward a food distribution center (based on their
 * knowledge of available centers) or to remain at home. Agents try to choose
 * the cheapest source of energy, preferring a closer center to a farther one.
 * If the agents project that getting the food will cost as much energy as the
 * food itself can provide, they will not move.
 * <p/>
 * Agents expend energy to move, in quantities that vary based on the quality of
 * the agent's path and the activity of the crowd. E.g., a paved road "costs"
 * less than an unpaved road, and participating in a riot costs even more
 * energy. If an agent's energy level dips below 0, it dies.
 */
public class Agent implements Steppable
{
    private static final long serialVersionUID = 1L;

    // ATTRIBUTES

    double energyLevel;

    boolean hasFood = false; // whether the agent has received food


    int activity = ACTION_STAY_HOME; // updated so that the observer can tell what the agent is currently doing

    // info about centers and whether they have food

    int centerInfo = 0; // an efficient way to store information about all of the centers the agent believes
    // are distributing food. The information is stored as bits, so that if an Agent knows about both
    // Center 1 and Center 3 it sets its Center Info to the number defined by (Center 1 && Center 3),
    // or (0001 && 0100) to get (0101) or a centerInfo of 5. This takes advantage of the convenience of
    // bitwise operations and speeds up the simulation.


    int[] powers =
    {
        1, 2, 4, 8, 16, 32, 64, 128
    };


    // info about where the agent is, where it's going, and where its home is
    Location position = null;


    Location home = null;


    Location goal = null;

    // the amount of time since the agent has confirmed that its course of
    // action is the best available to it
    int timeSinceReevaluate = 0;


    int reevaluateInterval = 1;


    ArrayList<Location> path = null; // the agent's current path to its current goal

    // ENERGY PARAMTERS
    // the parameters that indicate how much energy is used every tick the agent
    // does one of these activities. If we want them to die faster, INCREASE THESE COSTS

    public static double ENERGY_TO_STAY = .1;


    public static double ENERGY_TO_WALK_PAVED = .5;


    public static double ENERGY_TO_WALK_UNPAVED = 1.;


    public static double ENERGY_TO_RIOT = 5.;

    // ACTIVITY OPTIONS

    public static int ACTION_STAY_HOME = 0;


    public static int ACTION_GO_TO_CENTER = 1;


    public static int ACTION_GO_HOME = 2;

    // OTHER

    public Stoppable stopper = null; // used to unschedule the agent


    /**
     * @param home        - the home location
     * @param position    - the initial position of the agent
     * @param destruction - the level of destruction on the agent's home tile,
     *                    which impacts its initial energy levels
     */
    public Agent(Location home, Location position, int destruction)
    {
        this.home = home;
        this.position = position;

        energyLevel = energyInitialization(destruction);
    }


    public Agent(Location home, Location position, int destruction, double enToStay,
                 double enWalkPaved, double enWalkUnpav, double enRiot, int interval)
    {
        this.home = home;
        this.position = position;
        energyLevel = energyInitialization(destruction);

        ENERGY_TO_STAY = enToStay;
        ENERGY_TO_WALK_PAVED = enWalkPaved;
        ENERGY_TO_WALK_UNPAVED = enWalkUnpav;
        ENERGY_TO_RIOT = enRiot;
        reevaluateInterval = interval;
    }


    /**
     * Calculates the energy level with which the agent should be initialized,
     * given its destruction. IF WE WANT THEM TO DIE FASTER, SET THESE INITIAL
     * ENERGIES LOWER
     * <p/>
     * @param destruction - the destruction level at the agent's home location
     * <p/>
     * @return the amount of energy with which the agent begins
     */
    double energyInitialization(int destruction)
    {

        // either no data / unclassified or no damage.
        if (destruction == 0 || destruction == 1)
        {
            return 2000;
        } else if (destruction == 2) // visible damage
        {
            return 1600;
        } else if (destruction == 3) // moderate damage
        {
            return 1200;
        } else if (destruction == 4) // significant damage
        {
            return 1000;
        }
        return 0;
    }


    /**
     * Confirm that the Agent is pursuing the best course of action. Checks
     * among the possible goals the agent might have and picks the best one,
     * given the agent's current food-holding status, location, and energy level
     * <p/>
     * ***MUCH MORE WORK CAN BE DONE HERE in terms of agent perception,
     * estimation, etc. ***
     */
    void checkGoal(HaitiFood state)
    {

        timeSinceReevaluate = 0; // we have checked just now!

        //
        // Check to see if we're on a course that shouldn't be interrupted
        //

        // we're home and fed. Don't go back out
        if (hasFood && position.equals(home))
        {
            activity = ACTION_STAY_HOME;
            goal = null;
            path = null;
            return;
        } // we're going home, so keep on keepin' on: we wouldn't be going home unless we'd
        // gotten food or run out of energy
        else if (goal != null && goal.equals(home))
        {
            activity = ACTION_GO_HOME;
            return;
        } // the Agent has procured food, and needs to go home
        else if (hasFood && goal != null)
        { // it has food and it's not gotten home yet
            activity = ACTION_GO_HOME;
            goal = home;
            path = null;
            return;
        } // we already have a plan and no new information
        else if (goal != null && centerInfo > 0)
        {
            return;
        }

        if (centerInfo == 0)
        {
            activity = ACTION_STAY_HOME;
            goal = null;
            return;
        }

        //
        // otherwise check for centers and go to the closest one
        //
        Location oldGoal = goal;
        goal = null;
        for (int i = 0; i < state.centersList.size(); i++)
        {

            if ((centerInfo & powers[i]) == 0)
            {
                continue; // don't know about that center yet!
            }
            Center c = state.centersList.get(i);

            // if not going anywhere else, try it
            if (goal == null)
            {
                goal = c.loc;
            } // otherwise if it's closer than the current goal, go to it!
            else if (position.distanceTo(c.loc) < position.distanceTo(goal))
            {
                goal = c.loc;
            }
        }

        //
        // if the NEAREST POSSIBLE GOAL is not too far, go to it. Otherwise set goal
        // to null and GO HOME
        //
        if (goal != null
            && //				goal.distanceTo(position) * ENERGY_TO_WALK_PAVED < energyLevel ){
            (goal.distanceTo(position) + goal.distanceTo(home)) * ENERGY_TO_WALK_PAVED < energyLevel)
        {
            if (goal != oldGoal)
            {
                path = null; // force a recalculation of path
            }
            activity = ACTION_GO_TO_CENTER;
        } else
        {
            goal = null;
            activity = ACTION_STAY_HOME; // don't go out
        }
    }


    /**
     * Unschedules the agent. Called once the agent has died.
     */
    void die(HaitiFood state)
    {

        // stop the agent from ticking
        stopper.stop();

        // remove this agent from the population
        state.population.remove(this);
        state.peopleList.remove(this);

        state.deaths_total++;
        state.deaths_this_tick++;
    }


    @Override
    public void step(SimState state)
    {

        HaitiFood hf = (HaitiFood) state;

        // check that the agent hasn't died of low energy
        if (energyLevel <= 0)
        {
            die(hf);
            return;
        }

        // confirm that current goal is worthwhile/the easiest target
        if (timeSinceReevaluate > reevaluateInterval)
        {
            checkGoal(hf);
        } else
        {
            timeSinceReevaluate++;
        }

        //
        // MOVE depending on the goal and path information
        //

        // if the agent has no goal, it does not move.
        if (goal == null)
        {
            energyLevel -= ENERGY_TO_STAY; // update its energy level
            return; // not doing anything
        } // otherwise, the agent moves toward its goal. Depending on how it
        // moves, it expends more or less energy
        else
        {

            if (position.equals(goal))
            { // we have reached our goal! No need to proceed further
                checkGoal(hf); // force a reconsideration of what we should be doing
                return; // take a breather
            }

            // make sure we have a path to the goal!
            if (path == null)
            {
                AStar astar = new AStar();
                path = astar.astarPath(hf,
                                       (Node) hf.closestNodes.get(position.x, position.y),
                                       (Node) hf.closestNodes.get(goal.x, goal.y));
                if (path != null)
                {
                    path.add(goal);
                }
            }


            // determine the best location to immediately move *toward*
            Location subgoal;

            // It's possible that the agent isn't close to a node that can take it to the center.
            // In that case, the A* will return null. If this is so the agent should move toward
            // the goal until such a node is found.
            if (path == null)
            {
                subgoal = goal;
            } // Otherwise we have a path and should continue to move along it
            else
            {

                // have we reached the end of an edge? If so, move to the next edge
                if (path.get(0).equals(position))
                {
                    path.remove(0);
                }

                // our current subgoal is the end of the current edge
                if (path.size() > 0)
                {
                    subgoal = path.get(0);
                } else
                {
                    subgoal = goal;
                }
            }

            // calculate the next tile to move *to*
            Location nextTile = getNextTile(hf, subgoal);

            // get a list of the people currently in the new place. If the density of that space
            // does not prohibit it, move there
            Bag density = hf.population.getObjectsAtLocation(nextTile.x, nextTile.y);
            if (density == null || density.size() < hf.maximumDensity)
            {

                energyLevel -= tileCost(position, hf);

                // transition between spaces
                hf.population.setObjectLocation(this, nextTile.x, nextTile.y);

                // update my position
                position.x = nextTile.x;
                position.y = nextTile.y;
            } // if we can't move to the new tile and we're in a riot, keep rioting
            else if (hf.population.getObjectsAtLocation(position.x, position.y).size() > hf.riotDensity)
            {
                energyLevel -= ENERGY_TO_RIOT;
            } // otherwise just stay put
            else
            {
                energyLevel -= ENERGY_TO_STAY;
            }

        }

        if (hf.population.getObjectsAtLocation(position.x, position.y).size() > hf.riotDensity)
        {
            hf.rioting++;
        }
    }


    /**
     * Measures the cost to the agent of moving through the given tile
     * <p/>
     * @param loc - the tile
     * @param hf
     * <p/>
     * @return the cost of moving through the given tile
     */
    double tileCost(Location loc, HaitiFood hf)
    {
        Bag people = hf.population.getObjectsAtLocation(loc.x, loc.y);
        if (people != null && people.size() > hf.riotDensity)
        {
            return ENERGY_TO_RIOT;
        }
        int roadType = ((IntGrid2D)hf.roads.getGrid()).get(loc.x, loc.y);
        if (roadType < hf.noRoadValue)
        {
            return ENERGY_TO_WALK_UNPAVED;
        } else
        {
            return ENERGY_TO_WALK_PAVED;
        }
    }


    /**
     * Given the subgoal toward which the agent is moving, determine the next
     * tile in the von Neumann neighborhood to which the agent should move,
     * weighting roads tiles as preferable to non-roads
     * <p/>
     * @param world
     * @param subgoal
     * <p/>
     * @return
     */
    Location getNextTile(HaitiFood world, Location subgoal)
    {

        // move in which direction?
        int moveX = 0, moveY = 0;
        int dx = subgoal.x - position.x;
        int dy = subgoal.y - position.y;
        if (dx < 0)
        {
            moveX = -1;
        } else if (dx > 0)
        {
            moveX = 1;
        }
        if (dy < 0)
        {
            moveY = -1;
        } else if (dy > 0)
        {
            moveY = 1;
        }

        // can either move in Y direction or X direction: see which is better
        Location xmove = (Location) world.locations.get(position.x + moveX, position.y);
        Location ymove = (Location) world.locations.get(position.x, position.y + moveY);

        boolean xmoveToRoad = ((IntGrid2D) world.roads.getGrid()).get(xmove.x, xmove.y) > -1;
        boolean ymoveToRoad = ((IntGrid2D) world.roads.getGrid()).get(ymove.x, ymove.y) > -1;

        if (moveX == 0 && moveY == 0)
        { // we are ON the subgoal, so don't move at all!
            // both are the same result, so just return the xmove (which is identical)
            return xmove;
        } else if (moveX == 0) // this means that moving in the x direction is not a valid move: it's +0
        {
            return ymove;
        } else if (moveY == 0) // this means that moving in the y direction is not a valid move: it's +0
        {
            return xmove;
        } else if (xmoveToRoad == ymoveToRoad)
        { //equally good moves: pick randomly between them
            if (world.random.nextBoolean())
            {
                return xmove;
            } else
            {
                return ymove;
            }
        } else if (xmoveToRoad && moveX != 0) // x is a road: pick it
        {
            return xmove;
        } else if (ymoveToRoad && moveY != 0)// y is a road: pick it
        {
            return ymove;
        } else if (moveX != 0) // move in the better direction
        {
            return xmove;
        } else if (moveY != 0) // yes
        {
            return ymove;
        } else
        {
            return ymove; // no justification
        }
    }


}